﻿using NHibernate.Criterion;
using SimpleExpressionEvaluator.Utilities;
using SimpleServiceBus.Messages.Management.Monitoring;
using SimpleServiceBus.Persistence.Domain.Management.Entities;
using SimpleServiceBus.Persistence.Domain.Management.Repositories;
using SimpleServiceBus.Utilities;

namespace SimpleServiceBus.Persistence.NHibernate.Repositories
{
    public class EndpointStatusRepository : IEndpointStatusRepository
    {
        private readonly IRepository<ManagedEndpoint> _endpointRepo;
        private readonly IRepository<EndpointMetricType> _metricRepo;
        private readonly IRepository<EndpointPerformanceMeasurement> _perfMeasureRepo;
        private readonly IRepository<EndpointPerformanceSummary> _perfSummaryRepo;
        private readonly IRepository<EndpointError> _errorRepo;

        private static readonly object MetricLock = new object();
        private static readonly object EndpointLock = new object();

        public EndpointStatusRepository()
        {
            _endpointRepo = new Repository<ManagedEndpoint>();
            _metricRepo = new Repository<EndpointMetricType>();
            _perfSummaryRepo = new Repository<EndpointPerformanceSummary>();
            _perfSummaryRepo = new Repository<EndpointPerformanceSummary>();
            _errorRepo = new Repository<EndpointError>();
        }

        public EndpointStatusRepository(
            IRepository<ManagedEndpoint> endpointRepo,
            IRepository<EndpointMetricType> metricRepo,
            IRepository<EndpointPerformanceMeasurement> perfMeasureRepo,
            IRepository<EndpointPerformanceSummary> perfSummaryRepo,
            IRepository<EndpointError> errorRepo
            )
        {
            _endpointRepo = endpointRepo;
            _metricRepo = metricRepo;
            _perfMeasureRepo = perfMeasureRepo;
            _perfSummaryRepo = perfSummaryRepo;
            _errorRepo = errorRepo;
        }

        public bool PersistIncrementalStatusUpdates { get; set; }

        public void AddEndpointStatus(EndpointStatusUpdateNotification status)
        {
            using (new DataSessionUnitOfWork())
            {
                ManagedEndpoint endpoint = GetOrCreateEndpoint(status.EndpointID);

                endpoint.LastStatusUpdate = status.StatusTime;
                endpoint.Status = EndpointStatus.Online;
                endpoint.StatusNote = "Endpoint Online. Status updated @ " + status.StatusTime.ToLocalTime();

                _endpointRepo.SaveOrUpdate(endpoint);

                if (PersistIncrementalStatusUpdates)
                {
                    if (status.EndpointMeasurements.Count > 0)
                    {
                        foreach (EndpointMeasurement measurement in status.EndpointMeasurements)
                        {
                            var perf = new EndpointPerformanceMeasurement
                                           {
                                               MeasurementTime = measurement.Timestamp,
                                               MetricType = GetOrCreateMetricType(measurement),
                                               ManagedEndpoint = endpoint
                                           };
                            if (measurement.DataType == MeasurementDataType.Numeric)
                                perf.NumericValue = TypeNormalizer.EnsureType<double>(measurement.Value);
                            else if (measurement.DataType == MeasurementDataType.Boolean)
                                perf.BooleanValue = TypeNormalizer.EnsureType<bool>(measurement.Value);
                            else perf.TextValue = measurement.Value;
                            _perfMeasureRepo.SaveOrUpdate(perf);
                        }
                    }
                }
            }

        }

        public void AddEndpointError(EndpointErrorNotification error)
        {
            using (new DataSessionUnitOfWork())
            {
                ManagedEndpoint endpoint = GetOrCreateEndpoint(error.EndpointID);

                endpoint.LastStatusUpdate = error.StatusTime;
                endpoint.StatusNote = "Online - Exception Encountered:" + error.ErrorMessage.Elide(25);

                _endpointRepo.SaveOrUpdate(endpoint);

                var newError = new EndpointError
                                   {
                                       ManagedEndpoint = endpoint,
                                       ErrorMessage = error.ErrorMessage,
                                       Timestamp = error.StatusTime,
                                       FailedMessageID = error.FailedMessageID
                                   };
                _errorRepo.SaveOrUpdate(newError);
            }
        }

        public void UpdateEndpointOnlineStatus(EndpointOnlineNotification onlineNotification)
        {
            using (new DataSessionUnitOfWork())
            {
                ManagedEndpoint endpoint = GetOrCreateEndpoint(onlineNotification.EndpointID);
                endpoint.LastStatusUpdate = onlineNotification.StatusTime;
                endpoint.Status = EndpointStatus.Online;
                endpoint.StatusNote = "Endpoint Online @" + onlineNotification.StatusTime.ToLocalTime();
                endpoint.Name = onlineNotification.Name;
                endpoint.NetworkAddress = onlineNotification.NetworkAddress;
                endpoint.SupportEmailAddress = onlineNotification.SupportEmailAddress;
                endpoint.SupportTelephoneNumber = onlineNotification.SupportTelephoneNumber;
                endpoint.EndpointLatitude = onlineNotification.EndpointLatitude;
                endpoint.EndpointLongitude = onlineNotification.EndpointLongitude;

                _endpointRepo.SaveOrUpdate(endpoint);
            }

        }

        public void UpdateEndpointOfflineStatus(EndpointOfflineNotification offlineNotification)
        {
            using (new DataSessionUnitOfWork())
            {
                ManagedEndpoint endpoint = GetOrCreateEndpoint(offlineNotification.EndpointID);

                endpoint.LastStatusUpdate = offlineNotification.StatusTime;
                endpoint.Status = EndpointStatus.Offline;
                endpoint.StatusNote = offlineNotification.OfflineReason;

                _endpointRepo.SaveOrUpdate(endpoint);
            }
        }

        public void UpdateEndpointExpiredStatus(EndpointStatusExpiredNotification expiredNotification)
        {
            using (new DataSessionUnitOfWork())
            {
                ManagedEndpoint endpoint =
                    _endpointRepo.FindOne(Expression.Eq("EndpointID", expiredNotification.EndpointID));
                if (endpoint == null)
                    endpoint = new ManagedEndpoint {EndpointID = expiredNotification.EndpointID};
                endpoint.LastStatusUpdate = expiredNotification.LastStatusReceived;
                endpoint.Status = EndpointStatus.Unknown;
                endpoint.StatusNote =
                    "An expected heartbeat for this endpoint has not been received. This endpoint last reported its status @ " +
                    expiredNotification.LastStatusReceived.ToLocalTime();

                _endpointRepo.SaveOrUpdate(endpoint);
            }
        }

        public void UpdateStatusSummaries(EndpointStatusSummaryNotification summary)
        {
            
            if (summary == null || summary.ActiveEndpoints == null)
                return;

            using (new DataSessionUnitOfWork())
            {
                foreach (EndpointStatusSummary epSummary in summary.ActiveEndpoints)
                {
                    ManagedEndpoint endpoint = GetOrCreateEndpoint(epSummary.EndpointID);

                    if (epSummary.StatusMeasurements != null)
                    {
                        foreach (EndpointMeasurementSummary measurement in epSummary.StatusMeasurements)
                        {
                            var newSummary = new EndpointPerformanceSummary
                                                 {
                                                     ManagedEndpoint = endpoint,
                                                     SamplePeriodStartTime = summary.AggregationStartTime,
                                                     SamplePeriodEndTime = summary.AggregationStopTime,
                                                     Average = measurement.Average,
                                                     Max = measurement.Max,
                                                     Metric = GetOrCreateMetricType(measurement.LastMeasurement),
                                                     Min = measurement.Min,
                                                     SampleCount = measurement.SampleCount,
                                                     Sum = measurement.Sum
                                                 };

                            _perfSummaryRepo.SaveOrUpdate(newSummary);
                        }
                    }
                }
            }

        }

        private ManagedEndpoint GetOrCreateEndpoint(string endpointId)
        {
            lock (EndpointLock)
            {
                ManagedEndpoint endpoint = _endpointRepo.FindOne(Expression.Eq("EndpointID", endpointId)) ??
                                           new ManagedEndpoint {EndpointID = endpointId};
                _endpointRepo.SaveOrUpdate(endpoint);
                return endpoint;
            }
        }

        private EndpointMetricType GetOrCreateMetricType(IEndpointMeasurement measurement)
        {
            if (measurement == null)
                return null;

            lock (MetricLock)
            {
                EndpointMetricType metric = _metricRepo.FindOne(Expression.Eq("Name", measurement.Name));

                if (metric == null)
                {
                    metric = new EndpointMetricType
                                 {
                                     CanAggregate = measurement.CanAggregate,
                                     DataType = measurement.DataType,
                                     Name = measurement.Name,
                                     UnitOfMeasurement = measurement.UnitOfMeasurement
                                 };
                    _metricRepo.SaveOrUpdate(metric);
                }
                return metric;
            }
        }
    }
}
